﻿HmJS.$Import('core.fx');
HmJS.$Import('core.request');

HmJS.register('core.interface', function ($ns) {

	var document = window.document;

	// #region -- IframeShim --

	/**
	* name: IframeShim
	* 
	* description: Defines IframeShim, a class for obscuring select lists and flash objects in IE.
	* 
	* authors:
	*		- Aaron Newton
	* 
	* requires:
	*		- Core/Element.Event
	*		- Core/Element.Style
	*		- Core/Options
	*		- Core/Events
	*		- /Element.Position
	*		- /Class.Occlude
	* 
	* provides: [IframeShim]
	**/

	var IframeShim = this.IframeShim = new Class({

		Implements: [Options, Events, Class.Occlude],

		options: {
			className: 'iframeShim',
			src: 'javascript:false;document.write("");',
			display: false,
			zIndex: null,
			margin: 0,
			offset: { x: 0, y: 0 },
			browsers: (Browser.ie6 || (Browser.firefox && Browser.version < 3 && Browser.Platform.mac))
		},

		property: 'IframeShim',

		initialize: function (element, options) {
			this.element = document.id(element);
			if (this.occlude()) { return this.occluded; }
			this.setOptions(options);
			this.makeShim();
			return this;
		},

		makeShim: function () {
			if (this.options.browsers) {
				var zIndex = this.element.getStyle('zIndex').toInt();

				if (!zIndex) {
					zIndex = 1;
					var pos = this.element.getStyle('position');
					if (pos == 'static' || !pos) {
						this.element.setStyle('position', 'relative');
					}
					this.element.setStyle('zIndex', zIndex);
				}
				zIndex = ((this.options.zIndex != null || this.options.zIndex === 0) && zIndex > this.options.zIndex) ? this.options.zIndex : zIndex - 1;
				if (zIndex < 0) zIndex = 1;
				this.shim = new Element('iframe', {
					src: this.options.src,
					scrolling: 'no',
					frameborder: 0,
					styles: {
						zIndex: zIndex,
						position: 'absolute',
						border: 'none',
						filter: 'progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)'
					},
					'class': this.options.className
				}).store('IframeShim', this);
				var inject = (function () {
					this.shim.inject(this.element, 'after');
					this[this.options.display ? 'show' : 'hide']();
					this.fireEvent('inject');
				}).bind(this);
				if (!IframeShim.ready) {
					window.addEvent('load', inject);
				} else {
					inject();
				}
			} else {
				this.position = this.hide = this.show = this.dispose = Function.from(this);
			}
		},

		position: function () {
			if (!IframeShim.ready || !this.shim) {
				return this;
			}
			var size = this.element.measure(function () {
				return this.getSize();
			});
			if (this.options.margin != undefined) {
				size.x = size.x - (this.options.margin * 2);
				size.y = size.y - (this.options.margin * 2);
				this.options.offset.x += this.options.margin;
				this.options.offset.y += this.options.margin;
			}
			this.shim.set({ width: size.x, height: size.y }).position({
				relativeTo: this.element,
				offset: this.options.offset
			});
			return this;
		},

		hide: function () {
			if (this.shim) this.shim.setStyle('display', 'none');
			return this;
		},

		show: function () {
			if (this.shim) this.shim.setStyle('display', 'block');
			return this.position();
		},

		dispose: function () {
			if (this.shim) this.shim.dispose();
			return this;
		},

		destroy: function () {
			if (this.shim) this.shim.destroy();
			return this;
		}

	});

	window.addEvent('load', function () {
		IframeShim.ready = true;
	});

	// #endregion

	// #region -- Mask --

	/**
	* name: Mask
	* 
	* description: Creates a mask element to cover another.
	* 
	* authors:
	*		- Aaron Newton
	* 
	* requires:
	*		- Core/Options
	*		- Core/Events
	*		- Core/Element.Event
	*		- /Class.Binds
	*		- /Element.Position
	*		- /IframeShim
	* 
	* provides: [Mask]
	**/

	var Mask = this.Mask = new Class({

		Implements: [Options, Events],

		Binds: ['position'],

		options: {/*
		onShow: function(){},
		onHide: function(){},
		onDestroy: function(){},
		onClick: function(event){},
		inject: {
			where: 'after',
			target: null,
		},
		hideOnClick: false,
		id: null,
		destroyOnHide: false,*/
			style: {},
			'class': 'mask',
			maskMargins: false,
			useIframeShim: true,
			iframeShimOptions: {}
		},

		initialize: function (target, options) {
			this.target = document.id(target) || document.id(document.body);
			this.target.store('mask', this);
			this.setOptions(options);
			this.render();
			this.inject();
		},

		render: function () {
			this.element = new Element('div', {
				'class': this.options['class'],
				id: this.options.id || 'mask-' + String.uniqueID(),
				styles: Object.merge({}, this.options.style, {
					display: 'none'
				}),
				events: {
					click: function (event) {
						this.fireEvent('click', event);
						if (this.options.hideOnClick) this.hide();
					} .bind(this)
				}
			});

			this.hidden = true;
		},

		toElement: function () {
			return this.element;
		},

		inject: function (target, where) {
			where = where || (this.options.inject ? this.options.inject.where : '') || this.target == document.body ? 'inside' : 'after';
			target = target || (this.options.inject && this.options.inject.target) || this.target;

			this.element.inject(target, where);

			if (this.options.useIframeShim) {
				this.shim = new IframeShim(this.element, this.options.iframeShimOptions);

				this.addEvents({
					show: this.shim.show.bind(this.shim),
					hide: this.shim.hide.bind(this.shim),
					destroy: this.shim.destroy.bind(this.shim)
				});
			}
		},

		position: function () {
			this.resize(this.options.width, this.options.height);

			this.element.position({
				relativeTo: this.target,
				position: 'topLeft',
				ignoreMargins: !this.options.maskMargins,
				ignoreScroll: this.target == document.body
			});

			return this;
		},

		resize: function (x, y) {
			var opt = {
				styles: ['padding', 'border']
			};
			if (this.options.maskMargins) {
				opt.styles.push('margin');
			}

			var dim = this.target.getComputedSize(opt);
			if (this.target == document.body) {
				this.element.setStyles({ width: 0, height: 0 });
				var win = window.getScrollSize();
				if (dim.totalHeight < win.y) {
					dim.totalHeight = win.y;
				}
				if (dim.totalWidth < win.x) {
					dim.totalWidth = win.x;
				}
			}
			this.element.setStyles({
				width: Array.pick([x, dim.totalWidth, dim.x]),
				height: Array.pick([y, dim.totalHeight, dim.y])
			});

			return this;
		},

		show: function () {
			if (!this.hidden) { return this; }

			window.addEvent('resize', this.position);
			this.position();
			this.showMask.apply(this, arguments);

			return this;
		},

		showMask: function () {
			this.element.setStyle('display', 'block');
			this.hidden = false;
			this.fireEvent('show');
		},

		hide: function () {
			if (this.hidden) { return this; }

			window.removeEvent('resize', this.position);
			this.hideMask.apply(this, arguments);
			if (this.options.destroyOnHide) {
				return this.destroy();
			}

			return this;
		},

		hideMask: function () {
			this.element.setStyle('display', 'none');
			this.hidden = true;
			this.fireEvent('hide');
		},

		toggle: function () {
			this[this.hidden ? 'show' : 'hide']();
		},

		destroy: function () {
			this.hide();
			this.element.destroy();
			this.fireEvent('destroy');
			this.target.eliminate('mask');
		}

	});

	Element.Properties.mask = {

		set: function (options) {
			var mask = this.retrieve('mask');
			if (mask) { mask.destroy(); }
			return this.eliminate('mask').store('mask:options', options);
		},

		get: function () {
			var mask = this.retrieve('mask');
			if (!mask) {
				mask = new Mask(this, this.retrieve('mask:options'));
				this.store('mask', mask);
			}
			return mask;
		}

	};

	Element.implement({

		mask: function (options) {
			if (options) this.set('mask', options);
			this.get('mask').show();
			return this;
		},

		unmask: function () {
			this.get('mask').hide();
			return this;
		}

	});

	// #endregion

	// #region -- Spinner --

	/**
	* name: Spinner
	* 
	* description: Adds a semi-transparent overlay over a dom element with a spinnin ajax icon.
	* 
	* authors:
	*		- Aaron Newton
	* 
	* requires:
	*		- Core/Fx.Tween
	*		- Core/Request
	*		- /Class.refactor
	*		- /Mask
	* 
	* provides: [Spinner]
	**/

	var Spinner = this.Spinner = new Class({

		Extends: Mask,

		Implements: Chain,

		options: {/*
		message: false,*/
			'class': 'spinner',
			containerPosition: {},
			content: {
				'class': 'spinner-content'
			},
			messageContainer: {
				'class': 'spinner-msg'
			},
			img: {
				'class': 'spinner-img'
			},
			fxOptions: {
				link: 'chain'
			}
		},

		initialize: function (target, options) {
			this.target = document.id(target) || document.id(document.body);
			this.target.store('spinner', this);
			this.setOptions(options);
			this.render();
			this.inject();

			// Add this to events for when noFx is true; parent methods handle hide/show.
			var deactivate = function () { this.active = false; } .bind(this);
			this.addEvents({
				hide: deactivate,
				show: deactivate
			});
		},

		render: function () {
			this.parent();

			this.element.set('id', this.options.id || 'spinner-' + String.uniqueID());

			this.content = document.id(this.options.content) || new Element('div', this.options.content);
			this.content.inject(this.element);

			if (this.options.message) {
				this.msg = document.id(this.options.message) || new Element('p', this.options.messageContainer).appendText(this.options.message);
				this.msg.inject(this.content);
			}

			if (this.options.img) {
				this.img = document.id(this.options.img) || new Element('div', this.options.img);
				this.img.inject(this.content);
			}

			this.element.set('tween', this.options.fxOptions);
		},

		show: function (noFx) {
			if (this.active) {
				return this.chain(this.show.bind(this));
			}
			if (!this.hidden) {
				this.callChain.delay(20, this);
				return this;
			}

			this.active = true;

			return this.parent(noFx);
		},

		showMask: function (noFx) {
			var pos = function () {
				this.content.position(Object.merge({
					relativeTo: this.element
				}, this.options.containerPosition));
			} .bind(this);

			if (noFx) {
				this.parent();
				pos();
			} else {
				if (!this.options.style.opacity) this.options.style.opacity = this.element.getStyle('opacity').toFloat();
				this.element.setStyles({
					display: 'block',
					opacity: 0
				}).tween('opacity', this.options.style.opacity);
				pos();
				this.hidden = false;
				this.fireEvent('show');
				this.callChain();
			}
		},

		hide: function (noFx) {
			if (this.active) {
				return this.chain(this.hide.bind(this));
			}
			if (this.hidden) {
				this.callChain.delay(20, this);
				return this;
			}
			this.active = true;
			return this.parent(noFx);
		},

		hideMask: function (noFx) {
			if (noFx) { return this.parent(); }
			this.element.tween('opacity', 0).get('tween').chain(function () {
				this.element.setStyle('display', 'none');
				this.hidden = true;
				this.fireEvent('hide');
				this.callChain();
			} .bind(this));
		},

		destroy: function () {
			this.content.destroy();
			this.parent();
			this.target.eliminate('spinner');
		}

	});

	Request = Class.refactor(Request, {

		options: {
			useSpinner: false,
			spinnerOptions: {},
			spinnerTarget: false
		},

		initialize: function (options) {
			this._send = this.send;
			this.send = function (options) {
				var spinner = this.getSpinner();
				if (spinner) spinner.chain(this._send.pass(options, this)).show();
				else this._send(options);
				return this;
			};
			this.previous(options);
		},

		getSpinner: function () {
			if (!this.spinner) {
				var update = document.id(this.options.spinnerTarget) || document.id(this.options.update);
				if (this.options.useSpinner && update) {
					update.set('spinner', this.options.spinnerOptions);
					var spinner = this.spinner = update.get('spinner');
					['complete', 'exception', 'cancel'].each(function (event) {
						this.addEvent(event, spinner.hide.bind(spinner));
					}, this);
				}
			}
			return this.spinner;
		}

	});

	Element.Properties.spinner = {

		set: function (options) {
			var spinner = this.retrieve('spinner');
			if (spinner) { spinner.destroy(); }
			return this.eliminate('spinner').store('spinner:options', options);
		},

		get: function () {
			var spinner = this.retrieve('spinner');
			if (!spinner) {
				spinner = new Spinner(this, this.retrieve('spinner:options'));
				this.store('spinner', spinner);
			}
			return spinner;
		}

	};

	Element.implement({

		spin: function (options) {
			if (options) { this.set('spinner', options); }
			this.get('spinner').show();
			return this;
		},

		unspin: function () {
			this.get('spinner').hide();
			return this;
		}

	});

	// #endregion

	// #region -- HtmlTable --

	/**
	* name: HtmlTable
	* 
	* description: Builds table elements with methods to add rows.
	* 
	* authors:
	*		- Aaron Newton
	* 
	* requires:
	*		- Core/Options
	*		- Core/Events
	*		- /Class.Occlude
	* 
	* provides: [HtmlTable]
	**/

	var HtmlTable = this.HtmlTable = new Class({

		Implements: [Options, Events, Class.Occlude],

		options: {
			properties: {
				cellpadding: 0,
				cellspacing: 0,
				border: 0
			},
			rows: [],
			headers: [],
			footers: []
		},

		property: 'HtmlTable',

		initialize: function () {
			var params = Array.link(arguments, { options: Type.isObject, table: Type.isElement, id: Type.isString });
			this.setOptions(params.options);
			if (!params.table && params.id) {
				params.table = document.id(params.id);
			}
			this.element = params.table || new Element('table', this.options.properties);
			if (this.occlude()) {
				return this.occluded;
			}
			this.build();
		},

		build: function () {
			this.element.store('HtmlTable', this);

			this.body = document.id(this.element.tBodies[0]) || new Element('tbody').inject(this.element);
			$$(this.body.rows);

			if (this.options.headers.length) {
				this.setHeaders(this.options.headers);
			} else {
				this.thead = document.id(this.element.tHead);
			}

			if (this.thead) {
				this.head = this.getHead();
			}
			if (this.options.footers.length) {
				this.setFooters(this.options.footers);
			}

			this.tfoot = document.id(this.element.tFoot);
			if (this.tfoot) {
				this.foot = document.id(this.tfoot.rows[0]);
			}

			this.options.rows.each(function (row) {
				this.push(row);
			}, this);
		},

		toElement: function () {
			return this.element;
		},

		empty: function () {
			this.body.empty();
			return this;
		},

		set: function (what, items) {
			var target = (what == 'headers') ? 'tHead' : 'tFoot',
					lower = target.toLowerCase();

			this[lower] = (document.id(this.element[target]) || new Element(lower).inject(this.element, 'top')).empty();
			var data = this.push(items, {}, this[lower], what == 'headers' ? 'th' : 'td');

			if (what == 'headers') {
				this.head = this.getHead();
			} else {
				this.foot = this.getHead();
			}

			return data;
		},

		getHead: function () {
			var rows = this.thead.rows;
			return rows.length > 1 ? $$(rows) : rows.length ? document.id(rows[0]) : false;
		},

		setHeaders: function (headers) {
			this.set('headers', headers);
			return this;
		},

		setFooters: function (footers) {
			this.set('footers', footers);
			return this;
		},

		update: function (tr, row, tag) {
			var tds = tr.getChildren(tag || 'td'), last = tds.length - 1;

			row.each(function (data, index) {
				var td = tds[index] || new Element(tag || 'td').inject(tr),
				content = (data ? data.content : '') || data,
				type = typeOf(content);

				if (data && data.properties) {
					td.set(data.properties);
				}
				if (/(element(s?)|array|collection)/.test(type)) {
					td.empty().adopt(content);
				} else {
					td.set('html', content);
				}

				if (index > last) {
					tds.push(td);
				} else {
					tds[index] = td;
				}
			});

			return {
				tr: tr,
				tds: tds
			};
		},

		push: function (row, rowProperties, target, tag, where) {
			if (typeOf(row) == 'element' && row.get('tag') == 'tr') {
				row.inject(target || this.body, where);
				return {
					tr: row,
					tds: row.getChildren('td')
				};
			}
			return this.update(new Element('tr', rowProperties).inject(target || this.body, where), row, tag);
		},

		pushMany: function (rows, rowProperties, target, tag, where) {
			return rows.map(function (row) {
				return this.push(row, rowProperties, target, tag, where);
			}, this);
		}

	});

	['adopt', 'inject', 'wraps', 'grab', 'replaces', 'dispose'].each(function (method) {
		HtmlTable.implement(method, function () {
			this.element[method].apply(this.element, arguments);
			return this;
		});
	});

	// #endregion

	// #region -- HtmlTable.Zebra --

	/**
	* name: HtmlTable.Zebra
	* 
	* description: Builds a stripy table with methods to add rows.
	* 
	* authors:
	*		- Harald Kirschner
	*		- Aaron Newton
	* 
	* requires:
	*		- /HtmlTable
	*		- /Element.Shortcuts
	*		- /Class.refactor
	* 
	* provides: [HtmlTable.Zebra]
	**/

	HtmlTable = Class.refactor(HtmlTable, {

		options: {
			classZebra: 'table-tr-odd',
			zebra: true,
			zebraOnlyVisibleRows: true
		},

		initialize: function () {
			this.previous.apply(this, arguments);
			if (this.occluded) { return this.occluded; }
			if (this.options.zebra) { this.updateZebras(); }
		},

		updateZebras: function () {
			var index = 0;
			Array.each(this.body.rows, function (row) {
				if (!this.options.zebraOnlyVisibleRows || row.isDisplayed()) {
					this.zebra(row, index++);
				}
			}, this);
		},

		setRowStyle: function (row, i) {
			if (this.previous) { this.previous(row, i); }
			this.zebra(row, i);
		},

		zebra: function (row, i) {
			return row[((i % 2) ? 'remove' : 'add') + 'Class'](this.options.classZebra);
		},

		push: function () {
			var pushed = this.previous.apply(this, arguments);
			if (this.options.zebra) { this.updateZebras(); }
			return pushed;
		}

	});

	// #endregion

	// #region -- HtmlTable.Sort --

	/**
	* name: HtmlTable.Sort
	* 
	* description: Builds a stripy, sortable table with methods to add rows.
	* 
	* authors:
	*		- Harald Kirschner
	*		- Aaron Newton
	*		- Jacob Thornton
	* 
	* requires:
	*		- Core/Hash
	*		- /HtmlTable
	*		- /Class.refactor
	*		- /Element.Delegation
	*		- /String.Extras
	*		- /Date
	* 
	* provides: [HtmlTable.Sort]
	**/

	HtmlTable = Class.refactor(HtmlTable, {

		options: {/*
		onSort: function(){}, */
			sortIndex: 0,
			sortReverse: false,
			parsers: [],
			defaultParser: 'string',
			classSortable: 'table-sortable',
			classHeadSort: 'table-th-sort',
			classHeadSortRev: 'table-th-sort-rev',
			classNoSort: 'table-th-nosort',
			classGroupHead: 'table-tr-group-head',
			classGroup: 'table-tr-group',
			classCellSort: 'table-td-sort',
			classSortSpan: 'table-th-sort-span',
			sortable: false,
			thSelector: 'th'
		},

		initialize: function () {
			this.previous.apply(this, arguments);
			if (this.occluded) { return this.occluded; }
			this.sorted = { index: null, dir: 1 };
			if (!this.bound) { this.bound = {}; }
			this.bound.headClick = this.headClick.bind(this);
			this.sortSpans = new Elements();
			if (this.options.sortable) {
				this.enableSort();
				if (this.options.sortIndex != null) {
					this.sort(this.options.sortIndex, this.options.sortReverse);
				}
			}
		},

		attachSorts: function (attach) {
			this.detachSorts();
			if (attach !== false) {
				this.element.addEvent('click:relay(' + this.options.thSelector + ')', this.bound.headClick);
			}
		},

		detachSorts: function () {
			this.element.removeEvents('click:relay(' + this.options.thSelector + ')');
		},

		setHeaders: function () {
			this.previous.apply(this, arguments);
			if (this.sortEnabled) { this.setParsers(); }
		},

		setParsers: function () {
			this.parsers = this.detectParsers();
		},

		detectParsers: function () {
			return this.head && this.head.getElements(this.options.thSelector).flatten().map(this.detectParser, this);
		},

		detectParser: function (cell, index) {
			if (cell.hasClass(this.options.classNoSort) || cell.retrieve('htmltable-parser')) {
				return cell.retrieve('htmltable-parser');
			}
			var thDiv = new Element('div');
			thDiv.adopt(cell.childNodes).inject(cell);
			var sortSpan = new Element('span', { 'class': this.options.classSortSpan }).inject(thDiv, 'top');
			this.sortSpans.push(sortSpan);
			var parser = this.options.parsers[index],
					rows = this.body.rows,
					cancel;
			switch (typeOf(parser)) {
				case 'function':
					parser = { convert: parser };
					cancel = true;
					break;

				case 'string':
					parser = parser;
					cancel = true;
					break;
			}
			if (!cancel) {
				HtmlTable.ParserPriority.some(function (parserName) {
					var current = HtmlTable.Parsers[parserName],
					match = current.match;
					if (!match) { return false; }
					for (var i = 0, j = rows.length; i < j; i++) {
						var cell = document.id(rows[i].cells[index]),
						text = cell ? cell.get('html').clean() : '';
						if (text && match.test(text)) {
							parser = current;
							return true;
						}
					}
				});
			}
			if (!parser) { parser = this.options.defaultParser; }
			cell.store('htmltable-parser', parser);
			return parser;
		},

		headClick: function (event, el) {
			if (!this.head || el.hasClass(this.options.classNoSort)) {
				return;
			}
			return this.sort(Array.indexOf(this.head.getElements(this.options.thSelector).flatten(), el) % this.body.rows[0].cells.length);
		},

		serialize: function () {
			var previousSerialization = this.previous.apply(this, arguments) || {};
			if (this.options.sortable) {
				previousSerialization.sortIndex = this.sorted.index;
				previousSerialization.sortReverse = this.sorted.reverse;
			}
			return previousSerialization;
		},

		restore: function (tableState) {
			if (this.options.sortable && tableState.sortIndex) {
				this.sort(tableState.sortIndex, tableState.sortReverse);
			}
			this.previous.apply(this, arguments);
		},

		setSortedState: function (index, reverse) {
			if (reverse != null) {
				this.sorted.reverse = reverse;
			} else if (this.sorted.index == index) {
				this.sorted.reverse = !this.sorted.reverse;
			} else {
				this.sorted.reverse = this.sorted.index == null;
			}

			if (index != null) { this.sorted.index = index; }
		},

		setHeadSort: function (sorted) {
			var head = $$(!this.head.length ? this.head.cells[this.sorted.index] : this.head.map(function (row) {
				return row.getElements(this.options.thSelector)[this.sorted.index];
			}, this).clean());
			if (!head.length) { return; }
			if (sorted) {
				head.addClass(this.options.classHeadSort);
				if (this.sorted.reverse) {
					head.addClass(this.options.classHeadSortRev);
				} else {
					head.removeClass(this.options.classHeadSortRev);
				}
			} else {
				head.removeClass(this.options.classHeadSort).removeClass(this.options.classHeadSortRev);
			}
		},

		setRowSort: function (data, pre) {
			var count = data.length,
					body = this.body,
					group, rowIndex;

			while (count) {
				var item = data[--count],
						position = item.position,
						row = body.rows[position];

				if (row.disabled) { continue; }
				if (!pre) {
					group = this.setGroupSort(group, row, item);
					this.setRowStyle(row, count);
				}
				body.appendChild(row);

				for (rowIndex = 0; rowIndex < count; rowIndex++) {
					if (data[rowIndex].position > position) {
						data[rowIndex].position--;
					}
				}
			}
		},

		setRowStyle: function (row, i) {
			this.previous(row, i);
			row.cells[this.sorted.index].addClass(this.options.classCellSort);
		},

		setGroupSort: function (group, row, item) {
			if (group == item.value) {
				row.removeClass(this.options.classGroupHead).addClass(this.options.classGroup);
			} else {
				row.removeClass(this.options.classGroup).addClass(this.options.classGroupHead);
			}
			return item.value;
		},

		getParser: function () {
			var parser = this.parsers[this.sorted.index];
			return typeOf(parser) == 'string' ? HtmlTable.Parsers[parser] : parser;
		},

		sort: function (index, reverse, pre) {
			if (!this.head) { return; }

			if (!pre) {
				this.clearSort();
				this.setSortedState(index, reverse);
				this.setHeadSort(true);
			}

			var parser = this.getParser();
			if (!parser) { return; }

			var rel;
			if (!Browser.ie) {
				rel = this.body.getParent();
				this.body.dispose();
			}

			var data = this.parseData(parser).sort(function (a, b) {
				if (a.value === b.value) { return 0; }
				return a.value > b.value ? 1 : -1;
			});

			if (this.sorted.reverse == (parser == HtmlTable.Parsers['input-checked'])) {
				data.reverse(true);
			}
			this.setRowSort(data, pre);

			if (rel) { rel.grab(this.body); }
			this.fireEvent('stateChanged');
			return this.fireEvent('sort', [this.body, this.sorted.index]);
		},

		parseData: function (parser) {
			return Array.map(this.body.rows, function (row, i) {
				var value = parser.convert.call(document.id(row.cells[this.sorted.index]));
				return {
					position: i,
					value: value
				};
			}, this);
		},

		clearSort: function () {
			this.setHeadSort(false);
			this.body.getElements('td').removeClass(this.options.classCellSort);
		},

		reSort: function () {
			if (this.sortEnabled) {
				this.sort.call(this, this.sorted.index, this.sorted.reverse);
			}
			return this;
		},

		enableSort: function () {
			this.element.addClass(this.options.classSortable);
			this.attachSorts(true);
			this.setParsers();
			this.sortEnabled = true;
			return this;
		},

		disableSort: function () {
			this.element.removeClass(this.options.classSortable);
			this.attachSorts(false);
			this.sortSpans.each(function (span) {
				span.destroy();
			});
			this.sortSpans.empty();
			this.sortEnabled = false;
			return this;
		}

	});

	HtmlTable.ParserPriority = ['date', 'input-checked', 'input-value', 'float', 'number'];

	HtmlTable.Parsers = {

		'date': {
			match: /^\d{2}[-\/ ]\d{2}[-\/ ]\d{2,4}$/,
			convert: function () {
				var d = Date.parse(this.get('text').stripTags());
				return (typeOf(d) == 'date') ? d.format('db') : '';
			},
			type: 'date'
		},
		'input-checked': {
			match: / type="(radio|checkbox)" /,
			convert: function () {
				return this.getElement('input').checked;
			}
		},
		'input-value': {
			match: /<input/,
			convert: function () {
				return this.getElement('input').value;
			}
		},
		'number': {
			match: /^\d+[^\d.,]*$/,
			convert: function () {
				return this.get('text').stripTags().toInt();
			},
			number: true
		},
		'numberLax': {
			match: /^[^\d]+\d+$/,
			convert: function () {
				return this.get('text').replace(/[^-?^0-9]/, '').stripTags().toInt();
			},
			number: true
		},
		'float': {
			match: /^[\d]+\.[\d]+/,
			convert: function () {
				return this.get('text').replace(/[^-?^\d.]/, '').stripTags().toFloat();
			},
			number: true
		},
		'floatLax': {
			match: /^[^\d]+[\d]+\.[\d]+$/,
			convert: function () {
				return this.get('text').replace(/[^-?^\d.]/, '').stripTags();
			},
			number: true
		},
		'string': {
			match: null,
			convert: function () {
				return this.get('text').stripTags().toLowerCase();
			}
		},
		'title': {
			match: null,
			convert: function () {
				return this.title;
			}
		}

	};

	HtmlTable.defineParsers = function (parsers) {
		HtmlTable.Parsers = Object.append(HtmlTable.Parsers, parsers);
		for (var parser in parsers) {
			HtmlTable.ParserPriority.unshift(parser);
		}
	};

	// #endregion

	// #region -- Keyboard Class --

	/**
	* name: Keyboard
	* 
	* description: KeyboardEvents used to intercept events on a class for keyboard and format modifiers in a specific order so as to make alt+shift+c the same as shift+alt+c.
	* 
	* authors:
	*		- Perrin Westrich
	*		- Aaron Newton
	*		- Scott Kyle
	* 
	* requires:
	*		- Core/Events
	*		- Core/Options
	*		- Core/Element.Event
	*		- Element.Event.Pseudos.Keys
	* 
	* provides: [Keyboard]
	**/

	(function () {

		var Keyboard = this.Keyboard = new Class({

			Extends: Events,

			Implements: [Options],

			options: {/*
			onActivate: function(){},
			onDeactivate: function(){},*/
				defaultEventType: 'keydown',
				active: false,
				manager: null,
				events: {},
				nonParsedEvents: ['activate', 'deactivate', 'onactivate', 'ondeactivate', 'changed', 'onchanged']
			},

			initialize: function (options) {
				if (options && options.manager) {
					this._manager = options.manager;
					delete options.manager;
				}
				this.setOptions(options);
				this._setup();
			},

			addEvent: function (type, fn, internal) {
				return this.parent(Keyboard.parse(type, this.options.defaultEventType, this.options.nonParsedEvents), fn, internal);
			},

			removeEvent: function (type, fn) {
				return this.parent(Keyboard.parse(type, this.options.defaultEventType, this.options.nonParsedEvents), fn);
			},

			toggleActive: function () {
				return this[this.isActive() ? 'deactivate' : 'activate']();
			},

			activate: function (instance) {
				if (instance) {
					if (instance.isActive()) { return this; }
					//if we're stealing focus, store the last keyboard to have it so the relinquish command works
					if (this._activeKB && instance != this._activeKB) {
						this.previous = this._activeKB;
						this.previous.fireEvent('deactivate');
					}
					//if we're enabling a child, assign it so that events are now passed to it
					this._activeKB = instance.fireEvent('activate');
					Keyboard.manager.fireEvent('changed');
				} else if (this._manager) {
					//else we're enabling ourselves, we must ask our parent to do it for us
					this._manager.activate(this);
				}
				return this;
			},

			isActive: function () {
				return this._manager ? (this._manager._activeKB == this) : (Keyboard.manager == this);
			},

			deactivate: function (instance) {
				if (instance) {
					if (instance === this._activeKB) {
						this._activeKB = null;
						instance.fireEvent('deactivate');
						Keyboard.manager.fireEvent('changed');
					}
				} else if (this._manager) {
					this._manager.deactivate(this);
				}
				return this;
			},

			relinquish: function () {
				if (this.isActive() && this._manager && this._manager.previous) {
					this._manager.activate(this._manager.previous);
				} else {
					this.deactivate();
				}
				return this;
			},

			//management logic
			manage: function (instance) {
				if (instance._manager) {
					instance._manager.drop(instance);
				}
				this._instances.push(instance);
				instance._manager = this;
				if (!this._activeKB) {
					this.activate(instance);
				}
				return this;
			},

			drop: function (instance) {
				instance.relinquish();
				this._instances.erase(instance);
				if (this._activeKB == instance) {
					if (this.previous && this._instances.contains(this.previous)) {
						this.activate(this.previous);
					} else {
						this._activeKB = this._instances[0];
					}
				}
				return this;
			},

			trace: function () {
				Keyboard.trace(this);
			},

			each: function (fn) {
				Keyboard.each(this, fn);
			},

			/*
			PRIVATE METHODS
			*/

			_instances: [],

			_disable: function (instance) {
				if (this._activeKB == instance) {
					this._activeKB = null;
				}
			},

			_setup: function () {
				this.addEvents(this.options.events);
				//if this is the root manager, nothing manages it
				if (Keyboard.manager && !this._manager) {
					Keyboard.manager.manage(this);
				}
				if (this.options.active) {
					this.activate();
				} else {
					this.relinquish();
				}
			},

			_handle: function (event, type) {
				//Keyboard.stop(event) prevents key propagation
				if (event.preventKeyboardPropagation) { return; }

				var bubbles = !!this._manager;
				if (bubbles && this._activeKB) {
					this._activeKB._handle(event, type);
					if (event.preventKeyboardPropagation) { return; }
				}
				this.fireEvent(type, event);

				if (!bubbles && this._activeKB) {
					this._activeKB._handle(event, type);
				}
			}

		});

		var parsed = {};
		var modifiers = ['shift', 'control', 'alt', 'meta'];
		var regex = /^(?:shift|control|ctrl|alt|meta)$/;

		Keyboard.parse = function (type, eventType, ignore) {
			if (ignore && ignore.contains(type.toLowerCase())) {
				return type;
			}

			type = type.toLowerCase().replace(/^(keyup|keydown):/, function ($0, $1) {
				eventType = $1;
				return '';
			});

			if (!parsed[type]) {
				var key, mods = {};
				type.split('+').each(function (part) {
					if (regex.test(part)) {
						mods[part] = true;
					} else {
						key = part;
					}
				});

				mods.control = mods.control || mods.ctrl; // allow both control and ctrl

				var keys = [];
				modifiers.each(function (mod) {
					if (mods[mod]) { keys.push(mod); }
				});

				if (key) { keys.push(key); }
				parsed[type] = keys.join('+');
			}

			return eventType + ':keys(' + parsed[type] + ')';
		};

		Keyboard.each = function (keyboard, fn) {
			var current = keyboard || Keyboard.manager;
			while (current) {
				fn.run(current);
				current = current._activeKB;
			}
		};

		Keyboard.stop = function (event) {
			event.preventKeyboardPropagation = true;
		};

		Keyboard.manager = new Keyboard({
			active: true
		});

		Keyboard.trace = function (keyboard) {
			keyboard = keyboard || Keyboard.manager;
			var hasConsole = window.console && console.log;
			if (hasConsole) {
				console.log('the following items have focus: ');
			}
			Keyboard.each(keyboard, function (current) {
				if (hasConsole) {
					console.log(document.id(current.widget) || current.wiget || current);
				}
			});
		};

		var handler = function (event) {
			var keys = [];
			modifiers.each(function (mod) {
				if (event[mod]) { keys.push(mod); }
			});

			if (!regex.test(event.key)) {
				keys.push(event.key);
			}
			Keyboard.manager._handle(event, event.type + ':keys(' + keys.join('+') + ')');
		};

		document.addEvents({
			'keyup': handler,
			'keydown': handler
		});

	})();

	// #endregion Keyboard

	// #region -- Keyboard.Extras --

	/**
	* name: Keyboard.Extras
	* 
	* description: Enhances Keyboard by adding the ability to name and describe keyboard shortcuts, and the ability to grab shortcuts by name and bind the shortcut to different keys.
	* 
	* authors:
	*		- Perrin Westrich
	* 
	* requires:
	*		- /Keyboard
	*		- /MooTools.More
	* 
	* provides: [Keyboard.Extras]
	**/

	Keyboard.prototype.options.nonParsedEvents.combine(['rebound', 'onrebound']);

	Keyboard.implement({

		/*
		shortcut should be in the format of:
		{
		'keys': 'shift+s', // the default to add as an event.
		'description': 'blah blah blah', // a brief description of the functionality.
		'handler': function(){} // the event handler to run when keys are pressed.
		}
		*/
		addShortcut: function (name, shortcut) {
			this._shortcuts = this._shortcuts || [];
			this._shortcutIndex = this._shortcutIndex || {};

			shortcut.getKeyboard = Function.from(this);
			shortcut.name = name;
			this._shortcutIndex[name] = shortcut;
			this._shortcuts.push(shortcut);
			if (shortcut.keys) {
				this.addEvent(shortcut.keys, shortcut.handler);
			}
			return this;
		},

		addShortcuts: function (obj) {
			for (var name in obj) {
				this.addShortcut(name, obj[name]);
			}
			return this;
		},

		removeShortcut: function (name) {
			var shortcut = this.getShortcut(name);
			if (shortcut && shortcut.keys) {
				this.removeEvent(shortcut.keys, shortcut.handler);
				delete this._shortcutIndex[name];
				this._shortcuts.erase(shortcut);
			}
			return this;
		},

		removeShortcuts: function (names) {
			names.each(this.removeShortcut, this);
			return this;
		},

		getShortcuts: function () {
			return this._shortcuts || [];
		},

		getShortcut: function (name) {
			return (this._shortcutIndex || {})[name];
		}

	});

	Keyboard.rebind = function (newKeys, shortcuts) {
		Array.from(shortcuts).each(function (shortcut) {
			shortcut.getKeyboard().removeEvent(shortcut.keys, shortcut.handler);
			shortcut.getKeyboard().addEvent(newKeys, shortcut.handler);
			shortcut.keys = newKeys;
			shortcut.getKeyboard().fireEvent('rebound');
		});
	};


	Keyboard.getActiveShortcuts = function (keyboard) {
		var activeKBS = [], activeSCS = [];
		Keyboard.each(keyboard, [].push.bind(activeKBS));
		activeKBS.each(function (kb) { activeSCS.extend(kb.getShortcuts()); });
		return activeSCS;
	};

	Keyboard.getShortcut = function (name, keyboard, opts) {
		opts = opts || {};
		var shortcuts = opts.many ? [] : null,
		set = opts.many ? function (kb) {
			var shortcut = kb.getShortcut(name);
			if (shortcut) { shortcuts.push(shortcut); }
		} : function (kb) {
			if (!shortcuts) { shortcuts = kb.getShortcut(name); }
		};
		Keyboard.each(keyboard, set);
		return shortcuts;
	};

	Keyboard.getShortcuts = function (name, keyboard) {
		return Keyboard.getShortcut(name, keyboard, { many: true });
	};

	// #endregion Keyboard.Extras

	// #region -- HtmlTable.Select --

	/**
	* name: HtmlTable.Select
	* 
	* description: Builds a stripy, sortable table with methods to add rows. Rows can be selected with the mouse or keyboard navigation.
	* 
	* authors:
	*		- Harald Kirschner
	*		- Aaron Newton
	* 
	* requires:
	*		- /Keyboard
	*		- /Keyboard.Extras
	*		- /HtmlTable
	*		- /Class.refactor
	*		- /Element.Delegation
	*		- /Element.Shortcuts
	* 
	* provides: [HtmlTable.Select]
	**/

	HtmlTable = Class.refactor(HtmlTable, {

		options: {
			/*onRowFocus: function(){},
			onRowUnfocus: function(){},*/
			useKeyboard: true,
			classRowSelected: 'table-tr-selected',
			classRowHovered: 'table-tr-hovered',
			classSelectable: 'table-selectable',
			shiftForMultiSelect: true,
			allowMultiSelect: true,
			selectable: false,
			selectHiddenRows: false
		},

		initialize: function () {
			this.previous.apply(this, arguments);
			if (this.occluded) { return this.occluded; }

			this.selectedRows = new Elements();

			if (!this.bound) { this.bound = {}; }
			this.bound.mouseleave = this.mouseleave.bind(this);
			this.bound.clickRow = this.clickRow.bind(this);
			this.bound.activateKeyboard = function () {
				if (this.keyboard && this.selectEnabled) {
					this.keyboard.activate();
				}
			} .bind(this);

			if (this.options.selectable) { this.enableSelect(); }
		},

		empty: function () {
			this.selectNone();
			return this.previous();
		},

		enableSelect: function () {
			this.selectEnabled = true;
			this.attachSelects();
			this.element.addClass(this.options.classSelectable);
			return this;
		},

		disableSelect: function () {
			this.selectEnabled = false;
			this.attachSelects(false);
			this.element.removeClass(this.options.classSelectable);
			return this;
		},

		push: function () {
			var ret = this.previous.apply(this, arguments);
			this.updateSelects();
			return ret;
		},

		toggleRow: function (row) {
			return this[(this.isSelected(row) ? 'de' : '') + 'selectRow'](row);
		},

		selectRow: function (row, _nocheck) {
			//private variable _nocheck: boolean whether or not to confirm the row is in the table body
			//added here for optimization when selecting ranges
			if (this.isSelected(row) || (!_nocheck && !this.body.getChildren().contains(row))) {
				return;
			}
			if (!this.options.allowMultiSelect) { this.selectNone(); }

			if (!this.isSelected(row)) {
				this.selectedRows.push(row);
				row.addClass(this.options.classRowSelected);
				this.fireEvent('rowFocus', [row, this.selectedRows]);
				this.fireEvent('stateChanged');
			}

			this.focused = row;
			document.clearSelection();

			return this;
		},

		isSelected: function (row) {
			return this.selectedRows.contains(row);
		},

		getSelected: function () {
			return this.selectedRows;
		},

		getSelected: function () {
			return this.selectedRows;
		},

		serialize: function () {
			var previousSerialization = this.previous.apply(this, arguments) || {};
			if (this.options.selectable) {
				previousSerialization.selectedRows = this.selectedRows.map(function (row) {
					return Array.indexOf(this.body.rows, row);
				} .bind(this));
			}
			return previousSerialization;
		},

		restore: function (tableState) {
			if (this.options.selectable && tableState.selectedRows) {
				tableState.selectedRows.each(function (index) {
					this.selectRow(this.body.rows[index]);
				} .bind(this));
			}
			this.previous.apply(this, arguments);
		},

		deselectRow: function (row, _nocheck) {
			if (!this.isSelected(row) || (!_nocheck && !this.body.getChildren().contains(row))) {
				return;
			}

			this.selectedRows = new Elements(Array.from(this.selectedRows).erase(row));
			row.removeClass(this.options.classRowSelected);
			this.fireEvent('rowUnfocus', [row, this.selectedRows]);
			this.fireEvent('stateChanged');
			return this;
		},

		selectAll: function (selectNone) {
			if (!selectNone && !this.options.allowMultiSelect) {
				return;
			}
			this.selectRange(0, this.body.rows.length, selectNone);
			return this;
		},

		selectNone: function () {
			return this.selectAll(true);
		},

		selectRange: function (startRow, endRow, _deselect) {
			if (!this.options.allowMultiSelect && !_deselect) {
				return;
			}
			var method = _deselect ? 'deselectRow' : 'selectRow',
			rows = Array.clone(this.body.rows);

			if (typeOf(startRow) == 'element') {
				startRow = rows.indexOf(startRow);
			}
			if (typeOf(endRow) == 'element') {
				endRow = rows.indexOf(endRow);
			}
			endRow = endRow < rows.length - 1 ? endRow : rows.length - 1;

			if (endRow < startRow) {
				var tmp = startRow;
				startRow = endRow;
				endRow = tmp;
			}

			for (var i = startRow; i <= endRow; i++) {
				if (this.options.selectHiddenRows || rows[i].isDisplayed()) {
					this[method](rows[i], true);
				}
			}

			return this;
		},

		deselectRange: function (startRow, endRow) {
			this.selectRange(startRow, endRow, true);
		},

		getSelected: function () {
			return this.selectedRows;
		},

		/*
		Private methods:
		*/

		enterRow: function (row) {
			if (this.hovered) {
				this.hovered = this.leaveRow(this.hovered);
			}
			this.hovered = row.addClass(this.options.classRowHovered);
		},

		leaveRow: function (row) {
			row.removeClass(this.options.classRowHovered);
		},

		updateSelects: function () {
			Array.each(this.body.rows, function (row) {
				var binders = row.retrieve('binders');
				if (!binders && !this.selectEnabled) { return; }
				if (!binders) {
					binders = {
						mouseenter: this.enterRow.pass([row], this),
						mouseleave: this.leaveRow.pass([row], this)
					};
					row.store('binders', binders);
				}
				if (this.selectEnabled) {
					row.addEvents(binders);
				} else {
					row.removeEvents(binders);
				}
			}, this);
		},

		shiftFocus: function (offset, event) {
			if (!this.focused) {
				return this.selectRow(this.body.rows[0], event);
			}
			var to = this.getRowByOffset(offset, this.options.selectHiddenRows);
			if (to === null || this.focused == this.body.rows[to]) {
				return this;
			}
			this.toggleRow(this.body.rows[to], event);
		},

		clickRow: function (event, row) {
			var selecting = (event.shift || event.meta || event.control) && this.options.shiftForMultiSelect;
			if (!selecting && !(event.rightClick && this.isSelected(row) && this.options.allowMultiSelect)) {
				this.selectNone();
			}

			if (event.rightClick) {
				this.selectRow(row);
			} else {
				this.toggleRow(row);
			}

			if (event.shift) {
				this.selectRange(this.rangeStart || this.body.rows[0], row, this.rangeStart ? !this.isSelected(row) : true);
				this.focused = row;
			}
			this.rangeStart = row;
		},

		getRowByOffset: function (offset, includeHiddenRows) {
			if (!this.focused) { return 0; }
			var index = Array.indexOf(this.body.rows, this.focused);
			if ((index == 0 && offset < 0) || (index == this.body.rows.length - 1 && offset > 0)) {
				return null;
			}
			if (includeHiddenRows) {
				index += offset;
			} else {
				var limit = 0,
						count = 0;
				if (offset > 0) {
					while (count < offset && index < this.body.rows.length - 1) {
						if (this.body.rows[++index].isDisplayed()) {
							count++;
						}
					}
				} else {
					while (count > offset && index > 0) {
						if (this.body.rows[--index].isDisplayed()) {
							count--;
						}
					}
				}
			}
			return index;
		},

		attachSelects: function (attach) {
			attach = attach != null ? attach : true;

			var method = attach ? 'addEvents' : 'removeEvents';
			this.element[method]({
				mouseleave: this.bound.mouseleave,
				click: this.bound.activateKeyboard
			});

			this.body[method]({
				'click:relay(tr)': this.bound.clickRow,
				'contextmenu:relay(tr)': this.bound.clickRow
			});

			if (this.options.useKeyboard || this.keyboard) {
				if (!this.keyboard) {
					this.keyboard = new Keyboard();
				}
				if (!this.selectKeysDefined) {
					this.selectKeysDefined = true;
					var timer, held;

					var move = function (offset) {
						var mover = function (e) {
							clearTimeout(timer);
							e.preventDefault();
							var to = this.body.rows[this.getRowByOffset(offset, this.options.selectHiddenRows)];
							if (e.shift && to && this.isSelected(to)) {
								this.deselectRow(this.focused);
								this.focused = to;
							} else {
								if (to && (!this.options.allowMultiSelect || !e.shift)) {
									this.selectNone();
								}
								this.shiftFocus(offset, e);
							}

							if (held) {
								timer = mover.delay(100, this, e);
							} else {
								timer = (function () {
									held = true;
									mover(e);
								}).delay(400);
							}
						} .bind(this);
						return mover;
					} .bind(this);

					var clear = function () {
						clearTimeout(timer);
						held = false;
					};

					this.keyboard.addEvents({
						'keydown:shift+up': move(-1),
						'keydown:shift+down': move(1),
						'keyup:shift+up': clear,
						'keyup:shift+down': clear,
						'keyup:up': clear,
						'keyup:down': clear
					});

					var shiftHint = '';
					if (this.options.allowMultiSelect && this.options.shiftForMultiSelect && this.options.useKeyboard) {
						shiftHint = " (Shift multi-selects).";
					}

					this.keyboard.addShortcuts({
						'Select Previous Row': {
							keys: 'up',
							shortcut: 'up arrow',
							handler: move(-1),
							description: 'Select the previous row in the table.' + shiftHint
						},
						'Select Next Row': {
							keys: 'down',
							shortcut: 'down arrow',
							handler: move(1),
							description: 'Select the next row in the table.' + shiftHint
						}
					});

				}
				this.keyboard[attach ? 'activate' : 'deactivate']();
			}
			this.updateSelects();
		},

		mouseleave: function () {
			if (this.hovered) this.leaveRow(this.hovered);
		}

	});

	// #endregion

	// #region -- Scroller --

	/*
	* name: Scroller
	* 
	* description: Class which scrolls the contents of any Element (including the window) when the mouse reaches the Element's boundaries.
	* 
	* authors:
	*		- Valerio Proietti
	* 
	* requires:
	*		- Core/Events
	*		- Core/Options
	*		- Core/Element.Event
	*		- Core/Element.Dimensions
	*		- MooTools.More
	* 
	* provides: [Scroller]
	**/

	// 滚动的效果
	var Scroller = this.Scroller = new Class({

		Implements: [Events, Options],

		options: {
			// 使对象滚动的范围值
			area: 20,
			// 滚动速度
			velocity: 1,
			// 滚动位置改变时的事件
			onChange: function (x, y) {
				this.element.scrollTo(x, y);
			},
			fps: 50
		},

		// 构造函数
		initialize: function (element, options) {
			// 合并参数
			this.setOptions(options);
			// 作用的当前对象
			this.element = document.id(element);
			this.docBody = document.id(this.element.getDocument().body);
			// 监听鼠标事件的对象
			this.listener = (typeOf(this.element) != 'element') ? this.docBody : this.element;
			this.timer = null;
			this.bound = {
				attach: this.attach.bind(this),
				detach: this.detach.bind(this),
				getCoords: this.getCoords.bind(this)
			};
		},

		// 开始滚动
		start: function () {
			// 添加鼠标移动事件监听
			this.listener.addEvents({
				mouseover: this.bound.attach,
				mouseleave: this.bound.detach
			});
			return this;
		},

		// 停止滚动
		stop: function () {
			// 移除鼠标事件监听
			this.listener.removeEvents({
				mouseover: this.bound.attach,
				mouseleave: this.bound.detach
			});
			this.detach();
			// 清除计时器
			this.timer = clearInterval(this.timer);
			return this;
		},

		attach: function () {
			this.listener.addEvent('mousemove', this.bound.getCoords);
		},

		detach: function () {
			this.listener.removeEvent('mousemove', this.bound.getCoords);
			this.timer = clearInterval(this.timer);
		},

		// 取得鼠标坐标
		getCoords: function (event) {
			// 取鼠标坐标信息
			this.page = (this.listener.get('tag') == 'body') ? event.client : event.page;
			// 如果未启动计时器, 启动之, 定时循环执行scroll方法
			if (!this.timer) {
				this.timer = this.scroll.periodical(Math.round(1000 / this.options.fps), this);
			}
		},

		// 滚动处理
		scroll: function () {
			// 获取当前对象的尺寸,滚动条及坐标信息
			var size = this.element.getSize(),
					scroll = this.element.getScroll(),
					pos = this.element != this.docBody ? this.element.getOffsets() : { x: 0, y: 0 },
					scrollSize = this.element.getScrollSize(),
					change = { x: 0, y: 0 },
					top = this.options.area.top || this.options.area,
					bottom = this.options.area.bottom || this.options.area;

			// 分别处理坐标系上的方向
			for (var z in this.page) {
				// 如果鼠标在对象区域内,并且当前方向上的滚动位置不为0
				if (this.page[z] < (top + pos[z]) && scroll[z] != 0) {
					// 往回滚(由上面的比较可知结果为负值)
					change[z] = (this.page[z] - top - pos[z]) * this.options.velocity;
				}
				// 如果鼠标在右/下方向移动,并且当前对象不是刚好两屏
				else if (this.page[z] + bottom > (size[z] + pos[z]) && scroll[z] + size[z] != scrollSize[z]) {
					// 向前滚(由上面的比较可知结果为正值)
					change[z] = (this.page[z] - size[z] + bottom - pos[z]) * this.options.velocity;
				}
				change[z] = change[z].round();
			}
			// 只要有一个方向上的滚动值有需要改变,触发onChange事件,传递最终需要滚动到的坐标值
			if (change.y || change.x) {
				this.fireEvent('change', [scroll.x + change.x, scroll.y + change.y]);
			}
		}

	});

	// #endregion

	// #region -- Tips --

	/**
	* name: Tips
	* 
	* description: Class for creating nice tips that follow the mouse cursor when hovering an element.
	* 
	* authors:
	*		- Valerio Proietti
	*		- Christoph Pojer
	*		- Luis Merino
	* 
	* requires:
	*		- Core/Options
	*		- Core/Events
	*		- Core/Element.Event
	*		- Core/Element.Style
	*		- Core/Element.Dimensions
	*		- /MooTools.More
	* 
	* provides: [Tips]
	**/

	// 信息提示层
	(function () {

		var read = function (option, element) {
			return (option) ? (typeOf(option) == 'function' ? option(element) : element.get(option)) : '';
		};

		this.Tips = new Class({

			Implements: [Events, Options],

			options: {/*
				id: null,
				onAttach: function(element){},
				onDetach: function(element){},
				onBound: function(coords){},*/
				// 显示事件
				onShow: function () {
					this.tip.setStyle('display', 'block');
				},
				// 隐藏事件
				onHide: function () {
					this.tip.setStyle('display', 'none');
				},
				title: 'title',
				text: function (element) {
					return element.get('rel') || element.get('href');
				},
				// 显示延时
				showDelay: 100,
				// 隐藏延时
				hideDelay: 100,
				// 提示层样式名
				className: 'tip-wrap',
				// 提示层显示时相对于鼠标位置的偏移量
				offset: { x: 16, y: 16 },
				windowPadding: { x: 0, y: 0 },
				// 显示时是否不随鼠标移动
				fixed: false,
				waiAria: true
			},

			// 构造函数
			initialize: function () {
				// 利用Array.link自动选择参数
				var params = Array.link(arguments, {
					options: Type.isObject,
					elements: function (obj) {
						return obj != null;
					}
				});
				// 合并参数
				this.setOptions(params.options);
				if (params.elements) {
					this.attach(params.elements);
				}
				this.container = new Element('div', { 'class': 'tip' });

				if (this.options.id) {
					this.container.set('id', this.options.id);
					if (this.options.waiAria) {
						this.attachWaiAria();
					}
				}
			},

			toElement: function () {
				if (this.tip) { return this.tip; }

				this.tip = new Element('div', {
					'class': this.options.className,
					styles: {
						position: 'absolute',
						top: 0,
						left: 0
					}
				}).adopt(new Element('div', { 'class': 'tip-top' }),
							this.container,
							new Element('div', { 'class': 'tip-bottom' }));

				return this.tip;
			},

			attachWaiAria: function () {
				var id = this.options.id;
				this.container.set('role', 'tooltip');

				if (!this.waiAria) {
					this.waiAria = {
						show: function (element) {
							if (id) {
								element.set('aria-describedby', id);
							}
							this.container.set('aria-hidden', 'false');
						},
						hide: function (element) {
							if (id) {
								element.erase('aria-describedby');
							}
							this.container.set('aria-hidden', 'true');
						}
					};
				}
				this.addEvents(this.waiAria);
			},

			detachWaiAria: function () {
				if (this.waiAria) {
					this.container.erase('role');
					this.container.erase('aria-hidden');
					this.removeEvents(this.waiAria);
				}
			},

			attach: function (elements) {
				$$(elements).each(function (element) {
					var title = read(this.options.title, element),
							text = read(this.options.text, element);

					element.set('title', '').store('tip:native', title).retrieve('tip:title', title);
					element.retrieve('tip:text', text);
					this.fireEvent('attach', [element]);

					var events = ['enter', 'leave'];
					if (!this.options.fixed) {
						events.push('move');
					}

					events.each(function (value) {
						var event = element.retrieve('tip:' + value);
						if (!event) event = function (event) {
							this['element' + value.capitalize()].apply(this, [event, element]);
						} .bind(this);

						element.store('tip:' + value, event).addEvent('mouse' + value, event);
					}, this);
				}, this);

				return this;
			},

			detach: function (elements) {
				$$(elements).each(function (element) {
					['enter', 'leave', 'move'].each(function (value) {
						element.removeEvent('mouse' + value, element.retrieve('tip:' + value)).eliminate('tip:' + value);
					});

					this.fireEvent('detach', [element]);

					if (this.options.title == 'title') { // This is necessary to check if we can revert the title
						var original = element.retrieve('tip:native');
						if (original) element.set('title', original);
					}
				}, this);

				return this;
			},

			elementEnter: function (event, element) {
				clearTimeout(this.timer);
				this.timer = (function () {
					this.container.empty();

					['title', 'text'].each(function (value) {
						var content = element.retrieve('tip:' + value);
						var div = this['_' + value + 'Element'] = new Element('div', {
							'class': 'tip-' + value
						}).inject(this.container);
						if (content) this.fill(div, content);
					}, this);
					this.show(element);
					this.position((this.options.fixed) ? { page: element.getPosition()} : event);
				}).delay(this.options.showDelay, this);
			},

			elementLeave: function (event, element) {
				clearTimeout(this.timer);
				this.timer = this.hide.delay(this.options.hideDelay, this, element);
				this.fireForParent(event, element);
			},

			setTitle: function (title) {
				if (this._titleElement) {
					this._titleElement.empty();
					this.fill(this._titleElement, title);
				}
				return this;
			},

			setText: function (text) {
				if (this._textElement) {
					this._textElement.empty();
					this.fill(this._textElement, text);
				}
				return this;
			},

			fireForParent: function (event, element) {
				element = element.getParent();
				if (!element || element == document.body) {
					return;
				}
				if (element.retrieve('tip:enter')) {
					element.fireEvent('mouseenter', event);
				} else {
					this.fireForParent(event, element);
				}
			},

			elementMove: function (event, element) {
				this.position(event);
			},

			position: function (event) {
				if (!this.tip) { document.id(this); }

				var size = window.getSize(),
					scroll = window.getScroll(),
					tip = { x: this.tip.offsetWidth, y: this.tip.offsetHeight },
					props = { x: 'left', y: 'top' },
					bounds = { y: false, x2: false, y2: false, x: false },
					obj = {};

				for (var z in props) {
					obj[props[z]] = event.page[z] + this.options.offset[z];
					if (obj[props[z]] < 0) {
						bounds[z] = true;
					}
					if ((obj[props[z]] + tip[z] - scroll[z]) > size[z] - this.options.windowPadding[z]) {
						obj[props[z]] = event.page[z] - this.options.offset[z] - tip[z];
						bounds[z + '2'] = true;
					}
				}

				this.fireEvent('bound', bounds);
				this.tip.setStyles(obj);
			},

			fill: function (element, contents) {
				if (typeof contents == 'string') {
					element.set('html', contents);
				}
				else element.adopt(contents);
			},

			show: function (element) {
				if (!this.tip) { document.id(this); }
				if (!this.tip.getParent()) {
					this.tip.inject(document.body);
				}
				this.fireEvent('show', [this.tip, element]);
			},

			hide: function (element) {
				if (!this.tip) { document.id(this); }
				this.fireEvent('hide', [this.tip, element]);
			}

		});

	})();

	// #endregion

});
